It had been over two years since I last looked at them, but I returned to Haskell, GUI and GTK4 on Windows. This time, things were more straightforward. To set things up again, I added the following MSYS2 packages to the Stack-supplied MSYS2 with stack exec -- pacman -S:
| 
					 1 2 3 4 5 6  | 
						stack exec -- pacman -Syu stack exec -- pacman -S mingw-w64-x86_64-pkgconf  stack exec -- pacman -S mingw-w64-x86_64-gobject-introspection  stack exec -- pacman -S mingw-w64-x86_64-gtksourceview5  stack exec -- pacman -S mingw-w64-x86_64-gtk4  stack exec -- pacman -S mingw-w64-x86_64-atk  | 
					
and set PKG_CONFIG_PATH to the mingw64\lib\pkgconfig directory and XDG_DATA_DIRS to the mingw64\share directory (of the Stack-supplied MSYS2). I found it was necessary to install the MSYS2 packages before building the Haskell packages that depend upon them.
MSYS2 package mingw-w64-x86_64-gtk4 does not have a dependancy on mingw-w64-x86_64-atk, but Haskell package gi-gtk >= 4.0 has a dependency on gi-atk. I asked a question about that at the haskell-gi GitHub repository.
A basic gktTest
I named the example package gtkTest, as before, with Main.hs as follows (being the haskell-gi repository’s current “Hello! World” program):
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38  | 
						{-# LANGUAGE ImplicitParams      #-} {-# LANGUAGE OverloadedLabels    #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings   #-} module Main   ( main   ) where import           Control.Monad ( void ) import           Data.GI.Base ( AttrOp (..), new, set ) import qualified GI.Gtk as Gtk activate :: Gtk.Application -> IO () activate app = do   button <- new Gtk.Button     [ #label := "Click me"     , On #clicked ( ?self `set` [ #sensitive := False                                 , #label := "Thanks for clicking me"                                 ]                   )     ]   window <- new Gtk.ApplicationWindow     [ #application := app     , #title := "Hi there"     , #child := button     ]   window.show main :: IO () main = do   app <- new Gtk.Application     [ #applicationId := "haskell-gi.example"     , On #activate (activate ?self)     ]   void $ app.run Nothing  | 
					
with a package.yaml specifying dependencies:
| 
					 1 2 3 4  | 
						dependencies: - base >= 4.7 && < 5 - gi-gtk >= 4.0 - haskell-gi-base  | 
					
and with Stack project-level configuration:
| 
					 1 2 3 4 5 6  | 
						snapshot: lts-22.39 # GHC 9.6.6 extra-deps: - gi-gtk-4.0.9 - gi-gdk-4.0.9 - gi-gsk-4.0.8  | 
					
I sought to understand how this program worked and, in particular, the use of ?self and app.run.
new
new is used in connection with app, window and button. Module Data.GI.Base.Constructible provides a type class Constructible with a default instance:
| 
					 1 2 3 4 5 6  | 
						class Constructible obj (tag :: AttrOpTag) where   new :: MonadIO m => (ManagedPtr obj -> obj) -> [AttrOp obj tag] -> m obj instance {-# OVERLAPPABLE #-}   (GObject obj, tag ~ 'AttrConstruct) => Constructible obj tag where     new = constructGObject  | 
					
In the code extract above, and other extracts, I have renamed type variables for consistency between extracts.
GI.Gtk.Objects.Application.Application is an instance of GObject.
Data.GI.Base.GObject.constructGObject makes use of Data.GI.Base.Signals.on:
| 
					 1 2 3 4 5 6 7 8 9 10 11  | 
						on ::       forall obj info m. (GObject obj, MonadIO m, SignalInfo info)    => obj    -> SignalProxy obj info   -> ((?self :: obj) => HaskellCallbackType info)   -> m SignalHandlerId on o p c =   liftIO $ connectSignal @info o w SignalConnectBefore (proxyDetail p)  where    w :: obj -> HaskellCallbackType info   w parent = let ?self = parent in c  | 
					
and on binds the implicit parameter ?self.
Given the language extension ImplicitParams, the implicit parameter ?self can be passed to activate.
AttrOp
Data.GI.Base.Attributes.AttrOp is a type with a number of data constructors, including (:=) (assign a value to an attribute) and On (connect the given signal to a signal handler.).
| 
					 1 2 3 4 5  | 
						On ::       forall obj info (tag :: AttrOpTag). (GObject obj, SignalInfo info)    => SignalProxy obj info    -> ((?self :: obj) => HaskellCallbackType info)    -> AttrOp obj tag  | 
					
app.run Nothing
In the example, app.run means the same as #run app.
Given the language extension OverloadedRecordDot, app.run is equivalent to getField @"run" app.
Module GHC.Record provides type class HasField which promises getField: 
| 
					 1 2  | 
						class HasField t obj p | t obj -> p where   getField :: obj -> p  | 
					
Given the language extension VisibleTypeApplications, @"run" in getField @"run" app specifies the type t in type class HasField t obj p. Given the language extentions DataKinds, "run" is a type-level literal of kind String. As app is of type Application, that specifies type obj in type class HasField t obj p.
Module GI.Gtk.Objects.Application provides an instance of HasField
| 
					 1 2 3 4 5 6 7 8 9 10  | 
						import qualified Data.GI.Base.Overloading as O import qualified GHC.Records as R ... #if MIN_VERSION_base(4,13,0) instance ( info ~ ResolveApplicationMethod t Application          , O.OverloadedMethod info Application p          , R.HasField t Application p          ) => R.HasField t Application p where   getField = O.overloadedMethod @info #endif  | 
					
Including the instance head (HasField t Application p) as a constraint in the instance context is a ‘trick’ which permits polymorphic fields.
Data.GI.Base.Overloading.overloadedMethod is promised by type class OverloadedMethod: 
| 
					 1 2  | 
						class OverloadedMethod info obj signature where   overloadedMethod :: obj -> signature  | 
					
Given the language extension VisualTypeApplications, the @info in O.overloadedMethod @info specifies the type info in type class OverloadedMethod info obj s. info is constrained to be equivalent to ResolveApplicationMethod t Application. When t is the type literal "run", that is equal to ApplicationRunMethodInfo (see further below).
Module GI.Gio.Objects.Application provides an instance of OverloadedMethod:
| 
					 1 2 3 4 5  | 
						instance ( signature ~ (Maybe ([[Char]]) -> m Int32)          , MonadIO m          , IsApplication obj          ) => O.OverloadedMethod ApplicationRunMethodInfo obj signature where   overloadedMethod = applicationRun  | 
					
| 
					 1 2 3 4 5  | 
						applicationRun ::      (B.CallStack.HasCallStack, MonadIO m, IsApplication obj)   => obj   -> Maybe ([[Char]])    -> m Int32  | 
					
and module GI.Gtk.Objects.Application provides a type family:
| 
					 1 2 3 4 5 6  | 
						import qualified Data.Kind as DK import qualified GI.Gio.Objects.Application as Gio.Application ... type family ResolveApplicationMethod (t :: Symbol) (obj :: DK.Type) :: DK.Type where   ...   ResolveApplicationMethod "run" obj = Gio.Application.ApplicationRunMethodInfo  | 
					
A less basic gtkTest
I tried a less basic version of gtkTest, using a Grid of three Button values, with Main.hs:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52  | 
						{-# LANGUAGE ImplicitParams      #-} {-# LANGUAGE OverloadedLabels    #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings   #-} module Main   ( main   ) where import           Control.Monad ( void ) import           Data.GI.Base ( AttrOp (..), new, set ) import           Data.Text ( Text ) import qualified GI.Gtk as Gtk activate :: Gtk.Application -> IO () activate app = do   grid <- new Gtk.Grid []   button1 <- mkButton "Click me!"   button2 <- mkButton "No, click me!"   button3 <- mkButton "Better click me!"   #attach grid button1 0 0 1 1   #attach grid button2 1 0 1 1   #attach grid button3 0 1 2 1   window <- new Gtk.ApplicationWindow     [ #application := app     , #title := "Hi there"     , #child := grid     ]   window.show mkButton :: Text -> IO Gtk.Button mkButton label = new Gtk.Button   [ #label := label   , On #clicked ( ?self `set` [ #sensitive := False                               , #label := "Thanks for clicking me"                               ]                 )   , #hexpand := True   ] main :: IO () main = do   app <- new Gtk.Application     [ #applicationId := "haskell-gi.example"     , On #activate (activate ?self)     ]   void $ app.run Nothing  | 
					
The result was as below, but I had to specify #hexpand := True for the buttons to take advantage of the available horizontal space. That is because an ApplicationWindow is a single-child container while a Grid is a multi-child container; the default behaviour is different.

A gtkTest with input and output
I tried a further variation with input (two Entry values) and output (a Label value):
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82  | 
						{-# LANGUAGE ImplicitParams      #-} {-# LANGUAGE OverloadedLabels    #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings   #-} module Main   ( main   ) where import           Control.Monad ( void ) import           Data.GI.Base ( AttrOp (..), get, new, on, set ) import           Data.Text ( Text ) import qualified Data.Text as T import qualified Data.Text.Read as T import qualified GI.Gtk as Gtk activate :: Gtk.Application -> IO () activate app = do   grid <- new Gtk.Grid     [ #columnSpacing := 5     , #rowSpacing := 5     , #marginBottom := 5     , #marginEnd := 5     , #marginStart := 5     , #marginTop := 5     ]   labelHeight <- mkLabel "Height:"   labelRadius <- mkLabel "Radius:"   labelResult <- mkLabel "Volume:"   labelValue <- mkLabel "None"   entryHeight <- mkEntry   entryRadius <- mkEntry   let update = do         hText <- entryHeight `get` #text         rText <- entryRadius `get` #text         case (T.rational hText, T.rational rText) of           (Right (h, _), Right (r, _)) ->             labelValue `set` [ #label := T.show $ cone h r ]           (_, _) -> labelValue `set`             [ #label := "One or both of height and radius is invalid."]   void $ on entryHeight #changed update   void $ on entryRadius #changed update   #attach grid labelHeight 0 0 1 1   #attach grid labelRadius 0 1 1 1   #attach grid labelResult 0 2 1 1   #attach grid labelValue 1 2 1 1   #attach grid entryHeight 1 0 1 1   #attach grid entryRadius 1 1 1 1   window <- new Gtk.ApplicationWindow     [ #application := app     , #title := "Volume of a cone"     , #child := grid     ]   window.show mkLabel :: Text -> IO Gtk.Label mkLabel label = new Gtk.Label   [ #label := label   , #halign := Gtk.AlignStart   ] mkEntry :: IO Gtk.Entry mkEntry = new Gtk.Entry [ #halign := Gtk.AlignStart ] main :: IO () main = do   app <- new Gtk.Application     [ #applicationId := "haskell-gi.example"     , On #activate (activate ?self)     ]   void $ app.run Nothing cone :: Double -> Double -> Double cone h r = h * pi * r * r / 3.0  | 
					
The use of Data.Text.show requires Haskell package text >= 2.1.2.
The default value of halign is AlignFill, which centres when there is no meaningful way to stretch. 
The result was as below:
